16. Exercise: Add the Detail Screen

L8 30 Detail Screen SC

Update Note:
In the video above, in DetailFragment.kt file, inside onCreateView() function, it shows the following two deprecated lines of code:

  1. At timestamp 05:51, the way to set the binding lifecycle to itself has changed from binding.setLifecycleOwner(this) to binding.lifecycleOwner = this.
  1. At timestamp 06:43, the ViewModelProviders class shown is now deprecated

    binding.viewModel = ViewModelProviders.of( this, viewModelFactory).get(DetailViewModel::class.java).

    Instead, please use the ViewModelProvider as shown in the instructions below.

With all those properties to choose from, we sure could use a little more information to help us decide.

In this exercise, you'll add a detail screen that displays more information for a Mars property. You'll also add all the usual infrastructure for adding a click listener and navigating to the new screen.

There are a lot of steps, but we'll break them up so you can build and run your code in between. If you want to start at this step, you can download the code for this exercise from: Step.07-Exercise-Adding-the-Detail-Screen. You will find plenty of //TODO comments to help you complete things, which may be particularly useful here given how many steps we have.

We'll start by updating the ViewModel code and adding some data binding to the fragment layout:

  1. In DetailViewModel, remove the @Suppress("UNUSED_PARAMETER") annotation from the class declaration.


  2. Add an encapsulated selectedProperty LiveData variable, then set its value in an init block:

  private val _selectedProperty = MutableLiveData<MarsProperty>()

  val selectedProperty: LiveData<MarsProperty>
        get() = _selectedProperty

    init {
        _selectedProperty.value = marsProperty
    }


  1. In fragment_detail.xml, add a <data> block and declare a viewModel <variable> of type DetailViewModel:
    <data>
        <variable
            name="viewModel"
            type="com.example.android.marsrealestate.detail.DetailViewModel" />
    </data>



  1. In the main_photo_image ImageView, add an app:imageUrl attribute that binds to the imgSrcUrl for the selectedProperty:
app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"



  1. Bind the property_type_text TextView to viewModel.selectedProperty.type:
 android:text="@{viewModel.selectedProperty.type}"

and the price_value_text TextView to viewModel.selectedProperty.price, converted to a string value:

 android:text="@{String.valueOf(viewModel.selectedProperty.price)}"



  1. Build and run your code here, just to make sure you're on track. Nothing should change yet.

Add navigation code to the ViewModel:

  1. In OverviewViewModel, add an encapsulated LiveData variable for navigating to the selectedProperty detail screen:
    private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()

    val navigateToSelectedProperty: LiveData<MarsProperty>
        get() = _navigateToSelectedProperty
     


  1. Add a function to set _navigateToSelectedProperty to marsProperty and initiate navigation to the detail screen on button click:
    fun displayPropertyDetails(marsProperty: MarsProperty) {
        _navigateToSelectedProperty.value = marsProperty
    }

and you'll need to add displayPropertyDetailsComplete() to set _navigateToSelectedProperty to false once navigation is completed to prevent unwanted extra navigations:

 fun displayPropertyDetailsComplete() {
       _navigateToSelectedProperty.value = null
 } 



  1. Build your code and verify it still runs correctly. We've now got the structure in place in the ViewModel we'll need to trigger Navigation.

Next add a click listener:

  1. InPhotoGridAdapter, create an internal OnClickListener class with a lambda in its constructor that initializes a matching onClick function:

    class OnClickListener(val clickListener: (marsProperty: MarsProperty) -> Unit) {
       fun onClick(marsProperty:MarsProperty) = clickListener(marsProperty)
    } 


  2. Add an OnClickListener parameter to the PhotoGridAdapter class declaration:

    class PhotoGridAdapter(val onClickListener: OnClickListener) : 
                  ListAdapter<MarsProperty, PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {


  3. In onBindViewHolder(), set up onClickListener() to pass marsProperty on button click:

    holder.itemView.setOnClickListener {
      onClickListener.onClick(marsProperty)
    }


  4. InOverviewFragment, update the PhotosGridAdapter binding to add a click listener that passes the selected property to viewModel.displayPropertyDetails():

    binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener {
        viewModel.displayPropertyDetails(it)
    })
  5. Build and run your project here. If you set a breakpoint in the debugger, you can see that the click handler in onCreateView is being called when you tap on an image,

Make MarsProperty parcelable so it can be passed as an argument in navigation :

  1. In MarsProperty, make the class parcelable by extending it from Parcelable and adding the @Parcelize annotation:
@Parcelize
data class MarsProperty(
        val id: String,
        @Json(name = "img_src") val imgSrcUrl: String,
        val type: String,
        val price: Double) : Parcelable {

        }
    


  1. In nav_graph.xml, add an argument selectedProperty of type MarsProperty to detailFragment. You can do it in the navigation editor, but the xml should look like this:

    <argument
        android:name="selectedProperty"
        app:argType="com.example.android.marsrealestate.network.MarsProperty"/>           

Build the project to generate the SafeArgs actions. Finish up the navigation to the Detail screen

  1. In OverviewFragment, add an observer on navigateToSelectedProperty that calls navigate() to go to the detail screen when the MarsProperty is not null.

    and of course, we can't forget to call displayPropertyDetailsComplete() when navigation is done:

     viewModel.navigateToSelectedProperty.observe(this, Observer {
        if ( null != it ) {
           this.findNavController().navigate(OverviewFragmentDirections.actionShowDetail(it))
           viewModel.displayPropertyDetailsComplete()
        }
    })


  2. In DetailFragment, in onCreateView() create a marsProperty variable from the DetailFragmentArgs arguments, then use it to create a DetailViewModelFactory:

val marsProperty = DetailFragmentArgs.fromBundle(arguments!!).selectedProperty

 val viewModelFactory = DetailViewModelFactory(marsProperty, application)



  1. Use the factory to create a DetailViewModel and bind it to the viewModel:

    binding.viewModel = ViewModelProvider(
            this, viewModelFactory).get(DetailViewModel::class.java)


  2. Build and run the app and click on an image to open the Detail screen. Wow, great job!

And finally, the Details screen could use some improved formatting when displaying property information:

  1. Inside the MarsProperty class, create an isRental boolean, and set its value based on whether the property type is "rent":
val isRental
        get() = type == "rent"
   


  1. In DetailViewModel, create a transformation map, displayPropertyPrice, to convert selectedProperty's price to a displayable string:

    val displayPropertyPrice = Transformations.map(selectedProperty) {
    app.applicationContext.getString(
        when (it.isRental) {
            true -> R.string.display_price_monthly_rental
            false -> R.string.display_price
        }, it.price)
    }

    and a second transformation map, displayPropertyType, to display whether selectedProperty is for sale or rent:

    val displayPropertyType = Transformations.map(selectedProperty) {
    app.applicationContext.getString(R.string.display_type,
        app.applicationContext.getString(
            when(it.isRental) {
                true -> R.string.type_rent
                false -> R.string.type_sale
            }))
    }


  2. Replace the TextView android:text bindings in fragment_detail with the transformations defined in the viewModel

android:text="@{viewModel.displayPropertyType}"

and the same for price_value_text

android:text="@{viewModel.displayPropertyPrice}"


  1. Run the app and select a property to see its details. Now you can see how much it costs, and if it's for sale or rent, to help you better decide whether a permanent move to Mars is in your future…

If you get stuck, go back and watch the video again. Once you’re done, you can check your solution against the solution we’ve provided here: Step.07-Solution-Adding-the-Detail-Screen, or using this git diff.

Task Description:

Complete the tasks below to add a detail screen to display a Mars listing.

Task List:

Task Feedback:

Great job! Give yourself a big pat on the back!